﻿//// TODO: use Use (Use.All, Use.One, Use.Any)
//// TODO: popups, drag and drop, scroll bars
namespace Webinator.Impls
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Drawing;
    using System.Linq;
    using System.Threading;
    using Condition;
    using Enums;
    using ExtensionMethods;
    using HtmlAgilityPack;
    using Interfaces;
    using Toolboxes;
    using Util;
    using Webinator.DomTraversal;

    /// <summary>
    /// Implementation of IWebManager for WebDriver
    /// </summary>
    [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed. Suppression is OK here.")]
    internal class WebManager : IWebManager
    {
        internal readonly ConditionInvoker ConditionInvoker;
        private readonly AjaxToolbox _ajax;
        private readonly ICore _core;
        private readonly IHighlighter _highlighter;
        private readonly JavascriptToolbox _javascript;
        private readonly List<JavascriptCookie> _cookies = new List<JavascriptCookie>();
        private readonly IOperatingSystemDomain _osActions;
        private ILogUtil _log;
        private Process _process;
        private Point _viewportCoordinates;
        private Size _viewportDimensions;
        private WindowHelper.WindowState _windowState;

        private bool _closed;

        public WebManager(ICore core, IHighlighter highlighter, ILogUtil log, IOperatingSystemDomain osActions)
        {
            _javascript = new JavascriptToolbox(this);
            _ajax = new AjaxToolbox(_javascript, this);
            _core = core;
            _log = log;
            _highlighter = highlighter;
            _osActions = osActions;
            ConditionInvoker = new ConditionInvoker(_ajax, _javascript, this, _core);
        }

        public Config Config
        {
            get
            {
                return _core.Config;
            } 
            
            set
            {
                _core.Config = value;
            }
        }

        public ILogUtil Log
        {
            get { return _log; }
            set { _log = value; }
        }

        public Process Process 
        { 
            get { return _process ?? (_process = _osActions.Process); }
            set { _process = value; }
        }

        public Point ViewportCoordinates
        {
            get
            {
                // If browser window changed state or we haven't calculated the viewport yet:
                var currentWindowState = WindowHelper.GetWindowState(Process.MainWindowHandle);
                if ((_viewportCoordinates.X == 0 && _viewportCoordinates.Y == 0) 
                   || ((currentWindowState == WindowHelper.WindowState.Maximized 
                   || currentWindowState == WindowHelper.WindowState.Normal) 
                   && currentWindowState != WindowState))
                {
                    WindowState = currentWindowState;
                    _viewportCoordinates = _osActions.GetViewportCoordinates();
                }

                return _viewportCoordinates; 
            }

            set
            {
                _viewportCoordinates = value;
            }
        }

        public Size ViewportDimensions
        {
            get
            {
                if (_viewportDimensions.Height == 0 || _viewportDimensions.Width == 0)
                {
                    _viewportDimensions = GetViewportDimensions();
                }

                return _viewportDimensions;
            }

            set
            {
                _viewportDimensions = value;
            }
        }

        public WindowHelper.WindowState WindowState
        {
            get
            {
                if (_windowState == WindowHelper.WindowState.None)
                {
                    _windowState = _osActions.GetWindowState();
                }

                return _windowState;
            }

            set
            {
                _windowState = value;
            }
        }

        public DomNode Dom
        {
            get { return new DomNode(this); }
        }

        public object GetEngine()
        {
            return _core.GetCore();
        }

        public IWebManager AddCookie(string name, string value, string path, string domain)
        {
            AppendVerbose(string.Format("Add cookie: [name: {0}, value: {1}, path: {2}, domain: {3}]", name, value, path, domain));
            var cookie = new JavascriptCookie { Name = name, Value = value, Path = path, Domain = domain };
            _cookies.Add(cookie);
            
            if (Config.UseJavascript)
            {
                RunJavaScript(_javascript.GetJavascriptAddCookie(cookie));
            }
            else
            {
                _core.AddCookie(name, value, path, domain);
            }

            return this;
        }

        public IWebManager AppendText(Locator locator, string text, ICondition condition = null)
        {
            AppendVerbose(string.Format("Append text: [{0}] at locator: [{1}]", text, locator));
            
            var element = _core.FindElement(locator);
            _highlighter.HighlightElement(element);
            _log.LogScreenshot();
            
            Action action = () =>
            {
                _core.ElementSendkeys(element, _core.GetAttribute(element, "value") + text);
                _highlighter.HighlightElement(element, true);
            };
            ConditionInvoker.ExecuteConditionalAction(action, condition);
            
            return this;
        }

        public IWebManager Back()
        {
            AppendVerbose("Navigate browser back");
            _core.NavigateBack();
            _log.LogScreenshot();
            return this;
        }

        public IWebManager Clear(Locator locator, ICondition condition = null)
        {
            AppendVerbose(string.Format("Clear input at locator: [{0}]", locator));

            var element = _core.FindElement(locator);
            _highlighter.HighlightElement(element);

            Action action = () =>
                                        {
                                            _core.ElementClear(element);
                                            _log.LogScreenshot();
                                            _highlighter.HighlightElement(element, true);
                                        };
            ConditionInvoker.ExecuteConditionalAction(action, condition);

            return this;
        }

        public IWebManager Click(Locator locator, ICondition condition = null)
        {
            return SafeClick(locator, false, condition);
        }

        public IWebManager Click(Locator locator, bool expectAlert, ICondition condition = null)
        {
            return SafeClick(locator, expectAlert, condition);
        }

        public void CloseBrowser()
        {
            if (!_closed)
            {
                _core.CloseBrowser();
                _closed = true;
            }
        }

        public IWebManager ConfirmDialog()
        {
            AppendVerbose("Confirm dialog");
            _core.ConfirmDialog();
            return this;
        }

        public IWebManager DeleteCookie(string name)
        {
            AppendVerbose(string.Format("Delete cookie: {0}", name));
            if (Config.UseJavascript)
            {
                RunJavaScript(_javascript.GetJavascriptDeleteCookie(name));
            }
            else
            {
                _core.DeleteCookie(name);
            }

            var cookie = _cookies.FirstOrDefault(item => item.Name == name);
            if (cookie != null)
            {
                _cookies.Remove(cookie);
            }

            return this;
        }

        public IWebManager DismissDialog()
        {
            AppendVerbose("Dismiss dialog");
            _core.DismissDialog();
            return this;
        }

        public IWebManager DisplayMessageInBrowser(string message, int timeout = 2000)
        {
            AppendVerbose(string.Format("Display message in browser: {0}", message));
            _log.DisplayMessageInBrowser(message, timeout);
            _log.LogScreenshot();
            return this;
        }

        public object EvalJavaScript(string js)
        {
            return _core.EvaluateScript(js);
        }

        public bool Exists(Locator locator, int timeoutInMilliseconds = 5000)
        {
            return ConditionInvoker.WaitForSuccess(() => _core.QuickElementExists(locator), timeoutInMilliseconds);
        }

        public IWebManager Forward()
        {
            AppendVerbose("Navigate browser forward");
            _core.NavigateForward();
            _log.LogScreenshot();
            return this;
        }

        public string GetAttribute(Locator locator, string attribute)
        {
            var element = _core.FindElement(locator);
            var value = _core.GetAttribute(element, attribute);
            return value;
        }

        public Rectangle GetLocatorLocation(Locator locator)
        {
            return _core.GetLocatorLocation(locator);
        }

        public IEnumerable<Rectangle> GetLocatorLocations(Locator locator)
        {
            return _core.GetLocatorLocations(locator);
        }

        public int GetNodeCount(Locator locator)
        {
            var xpath = XPathToolbox.GetXpathFromLocator(locator, true);
            var nodes = XPathToolbox.GetNodes(PageSource(), xpath);
            var count = nodes == null ? 0 : nodes.Count;
            return count;
        }

        public HtmlNodeCollection GetHtmlNodes(Locator locator)
        {
            var xpath = XPathToolbox.GetXpathFromLocator(locator, true);
            return XPathToolbox.GetNodes(PageSource(), xpath);
        }

        public SelectedItem GetSelectedElementFromCollection(Locator locator)
        {
            var selected = GetSelectedBy(locator);
            return selected[0];
        }

        public IEnumerable<SelectedItem> GetSelectedElementsFromCollection(Locator locator)
        {
            return GetSelectedBy(locator);
        }

        public SelectedItem GetSelectedListItem(Locator locator)
        {
            return _core.GetSelectedItem(locator);
        }

        public IEnumerable<SelectedItem> GetSelectedListItems(Locator locator)
        {
            return _core.GetSelectedItems(locator);
        }

        public string GetText(Locator locator)
        {
            var element = _core.FindElement(locator);
            return _core.GetElementText(element);
        }

        public IWebManager GoToUrl(string url, ICondition condition)
        {
            AppendVerbose(string.Format("Navigating to: [{0}]", url));

            Action action = () =>
            {
                _core.NavigateToUrl(url);
                _log.LogScreenshot();
            };
            ConditionInvoker.ExecuteConditionalAction(action, condition);
            _log.LogScreenshot();

            return this;
        }

        public IWebManager Highlight(Locator locator, bool removeHighlight = false)
        {
            var highlightLogText = removeHighlight ? "Un-Highlight" : "Highlight";
            AppendVerbose(string.Format("{0} locator: [{1}]", highlightLogText, locator));

            var element = _core.FindElement(locator);
            _highlighter.HighlightElement(element, removeHighlight);
            return this;
        }

        public HtmlDocument HtmlSource()
        {
            return XPathToolbox.GetHtmlDocument(PageSource());
        }

        public bool IsSelected(Locator locator)
        {
            var element = _core.FindElement(locator);
            return _core.ElementIsChecked(element);
        }

        public bool IsVisible(Locator locator, int timeoutInMilliseconds = 5000)
        {
            object element;
            
            try
            {
                element = _core.FindElement(locator, timeoutInMilliseconds);
            }
            catch
            {
                return false;
            }

            if (element == null) return false;

            return _core.ElementIsDisplayed(element);
        }

        public IWebManager MaximizeScreen()
        {
            AppendVerbose("Maximizing browser window");
            _osActions.MaximizeWindow();
            return this;
        }

        public string PageSource()
        {
            return _core.GetPageSource();
        }

        public IWebManager Refresh()
        {
            AppendVerbose("Refresh page");
            _core.NavigateRefresh();
            _log.LogScreenshot();
            return this;
        }

        public IWebManager RunJavaScript(string js)
        {
            _core.ExecuteScript(js);
            return this;
        }

        public IWebManager SelectCheckbox(Locator locator, bool check, ICondition condition = null)
        {
            AppendVerbose(string.Format("Select checkbox: [{0}] set to: [{1}]", locator, check));
            var element = _core.FindElement(locator);

            if (_core.ElementIsChecked(element) == check)
            {
                return this;
            }

            _highlighter.HighlightElement(element);
            _log.LogScreenshot();

            Action action = () =>
            {
                do
                {
                    _core.ElementSelect(element);
                }
                while (_core.ElementIsChecked(element) != check);
                _highlighter.HighlightElement(element, true);
            };
            ConditionInvoker.ExecuteConditionalAction(action, condition);

            return this;
        }

        public IWebManager SelectListItem(Locator locator, ICondition condition, params Selector[] selectors)
        {
            foreach (var selector in selectors)
            {
                AppendVerbose(string.Format("Select item: [{0}] from list: [{1}]", selector, locator));
            }

            var list = _core.FindElement(locator);
            _highlighter.HighlightElement(list);
            _log.LogScreenshot();

            Action action = () => _core.SelectListItems(locator, selectors);
            ConditionInvoker.ExecuteConditionalAction(action, condition);

            _highlighter.HighlightElement(list, true);
            return this;
        }

        public IWebManager SelectListItem(Locator locator, params Selector[] selectors)
        {
            return SelectListItem(locator, null, selectors);
        }

        public IWebManager SelectRadio(Locator locator, ICondition condition = null)
        {
            AppendVerbose(string.Format("Select radio: [{0}]", locator));
            var element = _core.FindElement(locator);

            if (_core.ElementIsChecked(element))
            {
                return this;
            }

            _highlighter.HighlightElement(element);
            _log.LogScreenshot();

            Action action = () =>
            {
                do
                {
                    _core.ElementSelect(element);
                }
                while (_core.ElementIsChecked(element) != true);
                _highlighter.HighlightElement(element, true);
            };
            ConditionInvoker.ExecuteConditionalAction(action, condition);

            return this;
        }

        public IWebManager SendKeys(Locator locator, string text, ICondition condition = null)
        {
            AppendVerbose(string.Format("Send keys: [{0}] at locator: [{1}]", text, locator));

            var element = _core.FindElement(locator);
            _highlighter.HighlightElement(element);
            _log.LogScreenshot();

            Action action = () =>
                                {
                                    var i = 0;
                                    const int MaxRetries = 3;
                                    string temp;
                                    do
                                    {
                                        i++;
                                        _core.ElementSendkeys(element, text);
                                        temp = _core.GetAttribute(element, "value");
                                    } 
                                    while (!temp.Contains(text) && i <= MaxRetries);
                                    _highlighter.HighlightElement(element, true);
                                };
            ConditionInvoker.ExecuteConditionalAction(action, condition);
            return this;
        }

        public IWebManager SetAttribute(Locator locator, string attribute, string value)
        {
            AppendVerbose(string.Format("Set attribute: {0} at locator: [{1}] to: {2}", attribute, locator, value));

            var element = _core.FindElement(locator);
            _core.SetAttribute(element, attribute, value);

            return this;
        }

        public IWebManager SetResolution(int width, int height)
        {
            AppendVerbose(string.Format("Resize window to: [width: {0}, height: {1}]", width, height));
            WindowHelper.SetWindowPosition(Process.MainWindowHandle, new Size(width, height));
            return this;
        }

        public IWebManager SetText(Locator locator, string text)
        {
            AppendVerbose(string.Format("Set text at locator: [{0}] to: {1}", locator, text));
            SetAttribute(locator, "value", text);
            return this;
        }

        public IWebManager SwitchToWindow(SwitchBy switchBy, Comparison comparison, string argument)
        {
            AppendVerbose(string.Format("Switching to window by: [{0}:{1}]", switchBy, argument));
            _core.SwitchWindow(switchBy, comparison, argument);
            _log.LogScreenshot();

            return this;
        }

        public IWebManager SwitchToMainWindow()
        {
            AppendVerbose("Switching to main window");
            _core.SwitchToMainWindow();
            _log.LogScreenshot();

            return this;
        }

        public IWebManager SwitchToFrame(Locator locator)
        {
            AppendVerbose(string.Format("Switching to frame: [{0}]", locator));
            _core.SwitchToFrame(locator);

            return this;
        }

        public IWebManager SwitchToMainFrame()
        {
            AppendVerbose("Switching to main frame");
            _core.SwitchToMainFrame();

            return this;
        }

        public IWebManager CloseCurrentWindow()
        {
            AppendVerbose("Closing window...");
            _core.CloseWindow();
            _log.LogScreenshot();

            return this;
        }

        // TODO: should we provide option to screenshot viewport, window, or page?
        public IWebManager TakeScreenShot(string filename)
        {
            AppendVerbose(string.Format("Take screenshot and save to: {0}", filename));

            var bp = new BitmapParser();
            bp.GetScreenshotOfViewport(Process.MainWindowHandle, this).Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);

            return this;
        }

        public bool TextPresent(string text)
        {
            return _core.GetVisibleText().NormalizeSpace().Contains(text);
        }

        public string Title()
        {
            return _core.GetPageTitle();
        }

        public IWebManager TriggerEvent(Locator locator, EventIs eventIs, ICondition condition = null)
        {
            AppendVerbose(string.Format("Trigger event: [{0}] on locator: [{1}]", eventIs.ToString().ToLower(), locator));
            _log.LogScreenshot();

            Action action = () => { };
            if (locator.Use != Use.All)
            {
                var element = _core.FindElement(locator);
                action = () => _core.ElementTriggerEvent(element, eventIs);
            }
            else
            {
                var elements = (IEnumerable)_core.FindElements(locator);
                action = elements.Cast<object>()
                    .Aggregate(action, (current, element) => current + (() => _core.ElementTriggerEvent(element, eventIs)));
            }

            ConditionInvoker.ExecuteConditionalAction(action, condition);

            _log.LogScreenshot();

            return this;
        }

        public string Url()
        {
            return _core.GetPageUrl();
        }

        public string VisibleText()
        {
            return _core.GetVisibleText().NormalizeSpace();
        }

        public bool Wait(ICondition condition)
        {
            return Wait(null, condition); 
        }

        public bool Wait(Action action, ICondition condition)
        {
            return ConditionInvoker.ExecuteConditionalAction(action, condition, ConditionType.Wait);
        }

        private void AppendVerbose(string message)
        {
            if (Config.LogLevel != Config.AvailableLogLevels.Verbose) return;
            _log.Append("Verbose", message);
            Console.WriteLine(message);
        }

        private IWebManager SafeClick(Locator locator, bool expectAlert, ICondition condition)
        {
            AppendVerbose(string.Format("Click on locator: [{0}]", locator));

            var element = _core.FindElement(locator);
            _highlighter.HighlightElement(element);
            _log.LogScreenshot();

            // Need a special handler if the click action will display a dialog
            var dialogAppears = condition as DialogAppears;
            if (dialogAppears != null)
            {
                Action action = () =>
                {
                    if (Config.UseJquery)
                    {
                        _javascript.InjectJquery();
                        try
                        {
                            var e = _core.FindElement(locator);
                            _core.ElementTriggerEvent(e, EventIs.Click);
                        }
                        catch (TimeoutException)
                        {
                            // Swallow timeout exception since it's expected
                        }
                    }
                    else
                    {
                        if (Config.Browser == Config.AvailableBrowsers.InternetExplorer)
                        {
                            try
                            {
                                _core.ElementFocus(element);
                                GetLocatorLocation(locator).GetScreenCoordinates(this).GetMidpoint().DoubleClick(this);
                            }
                            catch
                            {
                            }
                        }
                        else
                        {
                            // Focus element
                            _core.ElementFocus(element);
                            GetLocatorLocation(locator).GetScreenCoordinates(this).GetMidpoint().Click(this);
                        }
                    }
                };
                ConditionInvoker.ExecuteConditionalAction(action, condition);

                return this;
            }

            var windowDisappears = condition as WindowDisappears;
            if (windowDisappears != null)
            {
                ConditionInvoker.ExecuteConditionalAction(() => _core.ElementClick(element), condition);
                return this;
            }

            ConditionInvoker.ExecuteConditionalAction(() => _core.ElementClick(element), condition);

            if (expectAlert)
            {
                //if (_core.DialogPresent()) return this;
                return this;
            }

            _highlighter.HighlightElement(element, true);
            _log.LogScreenshot();

            return this;
        }

        private List<SelectedItem> GetSelectedBy(Locator locator)
        {
            var selectedItems = new List<SelectedItem>();
            var elements = (ReadOnlyCollection<object>)_core.FindElements(locator);
            var selected = new List<object>();
            var index = new List<int>();

            // Find selected element in the collection
            for (var i = 0; i < elements.Count; i++)
            {
                var element = elements[i];
                if (_core.ElementIsChecked(element))
                {
                    selected.Add(element);
                    index.Add(i);
                    break;
                }
            }

            if (selected.Count == 0)
            {
                return null;
            }

            for (var i = 0; i < selected.Count; i++)
            {
                var element = selected[i];
                var selectedItem = new SelectedItem
                {
                    Text = _core.GetElementText(element),
                    Value = _core.GetAttribute(element, "value"),
                    Index = index[i]
                };
                selectedItems.Add(selectedItem);
            }

            return selectedItems;
        }

        private Size GetViewportDimensions()
        {
            const string Javascript = @"{var a;var b;if(typeof window.innerWidth!=""undefined""){a=window.innerWidth,b=window.innerHeight}else if(typeof document.documentElement!=""undefined""&&typeof document.documentElement.clientWidth!=""undefined""&&document.documentElement.clientWidth!=0){a=document.documentElement.clientWidth,b=document.documentElement.clientHeight}else{a=document.getElementsByTagName(""body"")[0].clientWidth,b=document.getElementsByTagName(""body"")[0].clientHeight}return[a,b]}";
            var dimensions = (ReadOnlyCollection<object>)EvalJavaScript(Javascript);
            return new Size((int)dimensions[0], (int)dimensions[1]);
        }
    }
}